<?php

/**
 * Класс строка. Здесь под строкой подразумевается последовательность символов в некоторой кодировке.
 */
class str extends AbstractObject implements ArrayAccess, Iterator, Countable, SeekableIterator
{
    protected $_encodedString = null;

    /**
     * @param string/str $value
     * @param string $encoding
     */
    function __construct($value, $encoding = null)
    {
    	if (is_object($value) && is_a($value, __CLASS__)) {
    		$this->__copy($value);
    		return;
    	}

    	parent::__construct();
        $this->__readonly_fields[] = 'encoding';
        $this->__readonly_fields[] = 'length';

        $encodings = array(
            "UTF-8" => "StrEncodingUTF8",
        );
        if (!isset($encoding)) {
            $encoding = Application::encoding();
        } else {
            if (! $this->allowEncoding($encoding)) {
                throw new ArgException();
            }
            $encoding = $encoding;
        }

        if (isset($encodings[$encoding])) {
            $class_name = $encodings[$encoding];
        } else {
            $class_name = "StrEncodingAbstract";
        }
        $this->_encodedString = new $class_name($value);
    }

    protected function __copy($obj)
    {
    	if (!(is_object($obj) && is_a($obj, __CLASS__))) {
    		throw new ArgException();
    	}
    	parent::__copy($obj);

    	$encodedStringClass = get_class($this->_encodedString);
        $this->_encodedString = new $encodedStringClass($obj->_encodedString);
    }

    protected function __set_value($value)
    {
        if (! is_scalar($value)) {
            throw new ArgException();
        }
        $this->_value = strval($value);
        $this->length_obsolete = true;

        return $this->_value;
    }

    static function allowEncoding($value)
    {
    	$value = new self($value);
        if (!in_array($value->toString(), mb_list_encodings())) {
            return false;
        }
        return true;
    }

    static function normalizeEncoding($value)
    {
    	$value = new self($value);
        if (! self::allowEncoding($value)) {
            throw new ArgException();
        }
        return $value->toString();
    }

    protected function __get_length()
    {
        if ($this->length_obsolete) {
            $this->_length = mb_strlen($this->_value, $this->_encoding);
        }
        return $this->_length;
    }

    /**
     * Меняет кодировку.
     *
     * @param string $new_encoding
     * @return str
     */
    function convertTo($new_encoding = null)
    {
        if ($new_encoding === $this->_encoding) {
            return $this;
        }
        $new_encoding = $this->normalizeEncoding($new_encoding);

        $this->_value = mb_convert_encoding($this->_value, $new_encoding, $this->_encoding);
        $this->length_obsolete = true;
        $this->_encoding = $new_encoding;

        return $this;
    }

    function __toString()
    {
        return $this->toString();
    }

    /**
     * Возвращает содержимое в Application::encoding().
     *
     * @return string
     */
    function toString()
    {
        $obj = clone $this;
        $obj->convertTo(Application::encoding());
        return $obj->_value;
    }

    /**
     * Возвращает символ по его номеру (с нуля).
     *
     * @param int $index
     * @return string
     */
    function charAt($index)
    {
        return $this->substr($index, 1)->_value;
    }

    /**
     * Возвращает символ в Application::encoding().
     *
     * @param int $index
     * @return string
     */
    function toChar($index)
    {
        return str($this->charAt($index), $this->_encoding)->convertTo(Application::encoding())->_value;
    }

    /**
     * Вставляет строку в конец.
     *
     * @param str/string $str
     * @return str
     */
    function append($str)
    {
        $tail = new self($str);
        $this->__set_value(self::concat($this, $tail, $this->_encoding)->_value);

        return $this;
    }

    /**
     * Вставляет строку в начало.
     *
     * @param str/string $str
     * @return str
     */
    function insertBefore($str)
    {
        $head = new self($str);
        $this->__set_value(self::concat($head, $this, $this->_encoding)->_value);

        return $this;
    }

    /**
     * Конкатенация строк.
     *
     * @param str/string $head
     * @param str/string $tail
     * @param string $output_encoding
     * @return str
     */
    static function concat($head, $tail, $output_encoding = null)
    {
        $head = new self($head->_value, $output_encoding);
        $tail = new self($tail->_value, $output_encoding);

        $new_value = $head->_value . $tail->_value;

        return new self($new_value, $output_encoding);
    }

    /**
     * Возвращает фрагмент строки
     *
     * @param int $start
     * @param int $length
     * @return str
     */
    function substr($start, $length = null)
    {
        $start = $this->normalizeIndex($start);
        if (! $this->isExistsIndex($start)) {
            throw new ArgException();
        }
        if (! is_null($length)) {
            $length = $this->normalizeInt($length);
        } else {
            $length = $this->_length - $start;
        }
        return new self(mb_substr($this->_value, $start, $length, $this->_encoding), $this->_encoding);
    }

    /**
     * Преобразует в строчные.
     *
     * @return str
     */
    function toLowerCase()
    {
        $this->__set_value(mb_strtolower($this->_value, $this->_encoding));
        return $this;
    }

    /**
     * Преобразует в прописные.
     *
     * @return str
     */
    function toUpperCase()
    {
        $this->__set_value(mb_strtoupper($this->_value, $this->_encoding));
        return $this;
    }

    /**
     * Режет строку в массив по разделителю.
     *
     * @param string/str $delimiter Разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function split($delimiter, $limit = null)
    {
    	return $this->_eregSplit(false, $delimiter, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function eregSplit($pattern, $limit = null)
    {
    	return $this->_eregSplit(true, $pattern, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону/разделителю.
     *
     * @access protected
     * @param bool $ereg Если true, то $delimiter воспринимается как regex-шаблон, иначе — как разделитель.
     * @param string/str $delimiter Шаблон/разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    protected function _eregSplit($ereg, $delimiter, $limit = null)
    {
    	if (isset($limit) && (!is_int($limit) || $limit < 0)) {
    		throw new ArgException();
    	}
    	if ($limit === 0) {
    		return array();
    	}
    	$delimiter = new self($delimiter);
    	try {
    		$delimiter->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return array(clone $this);
    	}
    	if (! $ereg) {
	    	$delimiter = self::ereg_escape($delimiter);
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	if ($limit) {
    	   $tmp_result = mb_split($delimiter->_value, $this->_value, $limit);
    	} else {
    	   $tmp_result = mb_split($delimiter->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	$result = array();
    	foreach ($tmp_result as $str) {
    		$result[] = new self($str, $this->_encoding);
    	}

    	return $result;
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function ereg($pattern)
    {
    	return $this->_ereg(false, $pattern);
    }

    /**
     * Нечувствительный к регистру аналог ereg().
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function eregi($pattern)
    {
    	return $this->_ereg(true, $pattern);
    }

    /**
     * Проверяет, содержит ли строка подстроку $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function contains($substr)
    {
        return ($this->indexOf($substr) >= 0);
    }

    /**
     * Проверяет, начинается ли строка с подстроки $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function startsWith($substr)
    {
        return ($this->indexOf($substr) == 0);
    }

    /**
     * Возвращает начальную позицию первого вложения подстроки $substr. Если вложения нет, — возвращает -1.
     *
     * @param string/str $substr
     * @param int[optional] $offset Смещение к концу относительно начала.
     * @return int
     */
    function indexOf($substr, $offset = null)
    {
    	$substr = new self($substr);
    	try {
            $substr->convertTo($this->_encoding);
    	} catch (Exception $e) {
    	    return -1;
    	}
    	if (isset($offset)) {
    	    if (!is_int($offset) || $offset < 0) {
        		throw new ArgException();
    	    }
        	if (($this->_length - $offset) < $substr->_length) {
        		return -1;
        	}
    	}
    	$result = mb_strpos($this->_value, $substr->_value, $offset, $this->_encoding);
    	if ($result === false) {
            $result = -1;
    	}
        return intval($result);
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Шаблон
     * @return bool
     */
    protected function _ereg($case_insensetive, $pattern)
    {
    	$pattern = new self($pattern);
    	try {
    		$pattern->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return false;
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->_value;
    	    $subject = str($this)->convertTo("UTF-8")->_value;
    		$result = preg_match($ppp, $subject);
    	} else {
    		$result = mb_ereg($pattern->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	return (bool)$result;
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    function eregReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(false, $pattern, $replacement);
    }

    /**
     * Тоже что и eregReplace(), только нечувствительная к регистру.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    function eregiReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(true, $pattern, $replacement);
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    protected function _eregReplace($case_insensetive, $pattern, $replacement)
    {
    	$pattern = new self($pattern);
    	$replacement = new self($replacement);
    	try {
    		$pattern->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return $this;
    	}
    	try {
    		$replacement->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		throw new Exception();
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	$tmp_result = "";
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->_value;
    	    $subject = str($this)->convertTo("UTF-8")->_value;
    		$tmp_result = preg_replace($ppp, $replacement->convertTo("UTF-8")->_value, $subject);
    		$tmp_result = str($tmp_result, "UTF-8")->convertTo($this->_encoding)->_value;
    	} else {
	    	$tmp_result = mb_ereg_replace($pattern->_value, $replacement->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	$this->__set_value($tmp_result);

    	return $this;
    }

    /**
     * Удаление пробельных символов в начале строки.
     *
     * @return str
     */
    function lTrim()
    {
    	return $this->eregReplace("^\s+", "");
    }

    /**
     * Удаление пробельных символов в конце строки.
     *
     * @return str
     */
    function rTrim()
    {
    	return $this->eregReplace("\s+$", "");
    }

    /**
     * Удаление пробельных символов по краям строки.
     *
     * @return str
     */
    function trim()
    {
    	return $this->lTrim()->rTrim();
    }

    /**
     * Удаление пробельных символов в начале и в конце строки.
     * Так же слитие пробельных последовательностей внутри строки в один пробел.
     *
     * @return str
     */
    function normalizeSpace()
    {
    	return $this->trim()->eregReplace("\s+", " ");
    }

    /**
     * Экранирование последовательности символов для вставки в regex.
     *
     * @return str
     */
    static function ereg_escape($expr)
    {
		$expr = new self($expr);
    	$chars = array(
    		"\\", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":",
		);
		$result = new self("", $expr->_encoding);
        for ($i = 0; $i < $expr->_length; $i++) {
            if (in_array($expr->toChar($i), $chars, true)) {
                $result->append(new self("\\"));
                $result->append($expr->charAt($i));
            } else {
                $result->append($expr->charAt($i));
            }
        }

		return $result;
    }

    /**
     * Разбивает текст на массив строк по признаку конца строки.
     *
     * @param string/str $data
     * @param string/str $EOL Признак конца строки (можно только "\r\n", "\r" и "\n").
     * @return array of string
     */
    function splitText($EOL = null)
    {
        if (isset($EOL)) {
            $EOL = str($EOL)->toString();
            if (! in_array($EOL, array("\r\n", "\r", "\n"), true)) {
        		throw new ArgException();
            }
        }
    	$tmp_lines = $this->split(isset($EOL) ? $EOL : "\r\n");
    	if (isset($EOL)) {
    	    $lines = $tmp_lines;
    	} else {
    	    $lines = array();
    	    foreach ($tmp_lines as $tmp_line) {
    	        foreach ($tmp_line->split("\r") as $tl) {
                    $lines = ar::concat($lines, $tl->split("\n"));
    	        }
    	    }
    	}

        return $lines;
    }

    /**
     * Заменяет все подстроки $searche подстроками $replacement.
     *
     * @param string/str $searche
     * @param string/str $replacement
     * @return str
     */
    function replace($searche, $replacement)
    {
    	$searche = new self($searche);
    	$replacement = new self($replacement);
    	try {
            $searche->convertTo($this->_encoding);
    	} catch (Exception $e) {
    	    return $this;
    	}
    	try {
            $replacement->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		throw new ArgException();
    	}
    	$searche = self::ereg_escape($searche);
        return $this->eregReplace($searche, $replacement);
    }

    /**
     * Преобразует символы в соответствующие XML-сущности.
     *
     * @param int $quote_style
     * ENT_COMPAT Преобразуются двойные кавычки, одиночные остаются без изменений.
     * ENT_QUOTES Преобразуются и двойные, и одиночные кавычки.
     * ENT_NOQUOTES И двойные, и одиночные кавычки остаются без изменений.
     *
     * @return str
     */
    function xmlEntities($quote_style = ENT_QUOTES)
    {
    	$this->replace("&", "&amp;")->replace("<", "&lt;")->replace(">", "&gt;");
    	if (($quote_style == ENT_QUOTES) || ($quote_style == ENT_COMPAT)) {
    	    $this->replace('"', "&quot;");
    	} elseif ($quote_style == ENT_QUOTES) {
    	    $this->replace("'", "&apos;");
    	}

    	return $this;
      }

    /**
     * Заменяет все управляющие символы (0—31) экранированными последовательностями \NN.
     *
     * @param string/str $addition_chars Строка с дополнительно экранируемыми символами (слеш будет экранирован всегда).
     * @return str
     */
    function addslashes($addition_chars = "")
    {
    	$addition_chars = new self($addition_chars);
    	$addition_chars_array = array();
        for ($i = 0; $i < $addition_chars->_length; $i++) {
            $addition_chars_array[] = $addition_chars->toChar($i);
        }
        $addition_chars_array[] = "\\";
    	$result = new self("");
        for ($i = 0; $i < $this->_length; $i++) {
            $char = $this->toChar($i);
            if (in_array($char, $addition_chars_array)) {
                $result->append(new self("\\"));
                $result->append($char);
            } elseif ((strlen($char) == 1) && (ord($char) < 32)) {
                $code = "";
                switch (ord($char)) {

                case 0x07:
                    $code = "a";
                    break;

                case 0x08:
                    $code = "b";
                    break;

                case 0x09:
                    $code = "t";
                    break;

                case 0x0A:
                    $code = "n";
                    break;

                case 0x0B:
                    $code = "v";
                    break;

                case 0x0C:
                    $code = "f";
                    break;

                case 0x0D:
                    $code = "r";
                    break;

                default:
                    $code = strval(intval(ord($char), 8));
                }
                $result->append(new self("\\"));
                $result->append($code);
            } else {
                $result->append($char);
            }
        }
        $this->__set_value($result->_value);

        return $this;
    }

    /**
     * Возвращает код символа в системе unicode по его номеру (с нуля).
     *
     * @param int $index
     * @return int
     */
    function charCodeAt($index)
    {
        $o = mb_convert_encoding($this->charAt($index), 'byte4be', $this->_encoding);
        return (ord($o[0]) << 24) + (ord($o[1]) << 16) + (ord($o[2]) << 8) + ord($o[3]);
    }

    /**
     * Возвращает код символа в системе Unicode.
     *
     * @param int $index
     * @return int
     */
    static function getCode($char)
    {
        return str($char)->charCodeAt(0);
    }

    /**
     * Возвращает символ по его коду Unicode.
     *
     * @param int $code
     * @return string
     */
    static function getChar($code)
    {
        if (! is_numeric($code)) {
            throw new ArgException();
        }
        $code = intval($code);
        if ($code < 0) {
            throw new ArgException();
        }
        $octets = chr(($code >> 24) % 256).chr(($code >> 16) % 256).chr(($code >> 8) % 256).chr($code % 256);
        return new self(mb_convert_encoding($octets, self::$innerEncoding, 'byte4be'), self::$innerEncoding);
    }

    /**
     * Создает строку из символов, заданных кодами Unicode.
     *
     * В JavaScrip’е этот метод принимает только двухбайтные коды
     * (если больше — берётся остаток от деления на 65536).
     * Так же как и эскейп-последовательности в строковом литерале \uHHHH.
     *
     * @param int $code1, $code2,… $codeN
     * @return string
     */
    static function fromCharCode()
    {
        $result = new self("", self::$innerEncoding);
        $args =& func_get_args();
        foreach ($args as $code) {
            $result->append(self::getChar($code));
        }
        return $result;
    }

    /**
     * Соединяет массив строк в одну с использованием разделителя.
     *
     * @param string $glue
     * @param array $pieces
     * @return int
     */
    static function implode($glue, $pieces)
    {
        if (! ar::isArray($pieces)) {
            throw new ArgException();
        }
        $glue = new self($glue);

        $result = new self("");

        $count = 0;
        foreach ($pieces as $v) {
            if ($count++) {
                $result->append($glue);
            }
            $result->append($v);
        }

        return $result;
    }

    /**
     * Возвращает строку из двоичных данных (октетов).
     *
     * @param string $stream
     * @return str
     */
    static function fromOctets($stream)
    {
        return new self($stream, 'ISO-8859-1');
    }

    /**
     * Нормализует целочисленное значение.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeInt($value)
    {
        if (! is_int($value)) {
            if (! is_numeric($value)) {
                throw new ArgException();
            } else {
                $value = intval($value);
            }
        }

        return $value;
    }

    /**
     * Нормализует индекс.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeIndex($index)
    {
        return $this->normalizeInt($index);
    }

    /**
     * Проверяет существует ли указанный индекс.
     *
     * @param int $index
     * @return bool
     */
    protected function isExistsIndex($index)
    {
        return (($index < $this->_length) && ($index >= 0));
    }

    /**
     * Реализация интерфейса прямой индексации ArrayAccess.
     */

    /**
     * Проверяет существует ли указанное смещение.
     *
     * @param mixed $index
     * @return bool
     */
    public function offsetExists($index)
    {
        $index = $this->normalizeIndex($index);
        return $this->isExistsIndex($index);
    }

    /**
     * Возврат значения по указанному смещению.
     *
     * @param mixed $index
     * @return string
     */
    public function offsetGet($index)
    {
        return $this->charAt($index);
    }

    /**
     * Установка символа в текущую позицию.
     *
     * @param mixed $index
     * @param string/str $value
     * @return string
     */
    public function offsetSet($index, $value)
    {
        $value = str($value);
        if (! $value->_length) {
            throw new ArgException();
        }
        if (isset($index)) {
            $index = $this->normalizeIndex($index);
            if (! $this->isExistsIndex($index)) {
                throw new ArgException();
            }
            $before = $this->substr(0, $index);
            $before->append($value->substr(0, 1));
            if ($this->isExistsIndex($index + 1)) {
                $before->append($this->substr($index + 1));
            }
            $this->__set_value($before->_value);
        } else {
            $this->append($value->substr(0, 1));
        }
    }

    /**
     * Заглушка для удаления символа из строки.
     *
     * @param mixed $index
     */
    public function offsetUnset($index)
    {
        throw new ArchException();
    }

    /**
     * Реализация интерфейса Iterator.
     */

    /**
     * Текущая позиция итератора.
     *
     * @var int
     */
    protected $iterator_index = 0;

    public function valid()
    {
        return $this->isExistsIndex($this->iterator_index);
    }

    public function current()
    {
        return $this->charAt($this->iterator_index);
    }

    public function key()
    {
        return $this->iterator_index;
    }

    public function next()
    {
        $this->iterator_index++;
    }

    public function rewind()
    {
        return $this->charAt($this->iterator_index = 0);
    }

    /**
     * Реализация интерфейса Countable.
     */

    /**
     * Возвращает кол-во символов в строке.
     *
     * @return int
     */
    public function count()
    {
        return $this->_length;
    }

    /**
     * Реализация интерфейса SeekableIterator.
     */

    /**
     * Переводит текущюю позицию итератора в значение $position.
     *
     * @param int $position
     */
    public function seek($position)
    {
        $position = $this->normalizeIndex($position);
        $this->iterator_index = $position;
    }
}

/*abstract*/ class istr extends str
{
    /**
     * Кодировка, в которой находится содержимое value.
     *
     * @var string
     */
    public $encoding;

    /**
     * Содержимое, закодираванное в кодировке encoding.
     *
     * @var string
     */
    public $value;

    /**
     * Длинна строки в символах.
     *
     * @var int
     */
    public $length;
}

class StrEncodingAbstract extends AbstractObject
{
    protected $_value = '';
    protected $_encoding = '';
    protected $_length = 0;
    protected $length_obsolete = false;

    static private $innerEncoding = 'UTF-8';

    /**
     * @param string/str $value
     * @param string $encoding
     */
    function __construct($value, $encoding = null)
    {
    	if (is_object($value) && is_a($value, __CLASS__)) {
    		$this->__copy($value);
    		return;
    	}

    	parent::__construct();
        $this->__readonly_fields[] = 'encoding';
        $this->__readonly_fields[] = 'length';
        if (!isset($encoding)) {
            $this->_encoding = Application::encoding();
        } else {
            if (! $this->allowEncoding($encoding)) {
                throw new ArgException();
            }
            $this->_encoding = $encoding;
        }

        $this->__set_value($value);
    }

    protected function __copy($obj)
    {
    	if (!(is_object($obj) && is_a($obj, __CLASS__))) {
    		throw new ArgException();
    	}
    	parent::__copy($obj);
        $this->_value          = $obj->_value;
        $this->_encoding       = $obj->_encoding;
        $this->_length         = $obj->_length;
        $this->length_obsolete = $obj->length_obsolete;
    }

    protected function __set_value($value)
    {
        if (! is_scalar($value)) {
            throw new ArgException();
        }
        $this->_value = strval($value);
        $this->length_obsolete = true;

        return $this->_value;
    }

    static function allowEncoding($value)
    {
    	$value = new self($value);
        if (!in_array($value->toString(), mb_list_encodings())) {
            return false;
        }
        return true;
    }

    static function normalizeEncoding($value)
    {
    	$value = new self($value);
        if (! self::allowEncoding($value)) {
            throw new ArgException();
        }
        return $value->toString();
    }

    protected function __get_length()
    {
        if ($this->length_obsolete) {
            $this->_length = mb_strlen($this->_value, $this->_encoding);
        }
        return $this->_length;
    }

    /**
     * Меняет кодировку.
     *
     * @param string $new_encoding
     * @return str
     */
    function convertTo($new_encoding = null)
    {
        if ($new_encoding === $this->_encoding) {
            return $this;
        }
        $new_encoding = $this->normalizeEncoding($new_encoding);

        $this->_value = mb_convert_encoding($this->_value, $new_encoding, $this->_encoding);
        $this->length_obsolete = true;
        $this->_encoding = $new_encoding;

        return $this;
    }

    function __toString()
    {
        return $this->toString();
    }

    /**
     * Возвращает содержимое в Application::encoding().
     *
     * @return string
     */
    function toString()
    {
        $obj = clone $this;
        $obj->convertTo(Application::encoding());
        return $obj->_value;
    }

    /**
     * Возвращает символ по его номеру (с нуля).
     *
     * @param int $index
     * @return string
     */
    function charAt($index)
    {
        return $this->substr($index, 1)->_value;
    }

    /**
     * Возвращает символ в Application::encoding().
     *
     * @param int $index
     * @return string
     */
    function toChar($index)
    {
        return str($this->charAt($index), $this->_encoding)->convertTo(Application::encoding())->_value;
    }

    /**
     * Вставляет строку в конец.
     *
     * @param str/string $str
     * @return str
     */
    function append($str)
    {
        $tail = new self($str);
        $this->__set_value(self::concat($this, $tail, $this->_encoding)->_value);

        return $this;
    }

    /**
     * Вставляет строку в начало.
     *
     * @param str/string $str
     * @return str
     */
    function insertBefore($str)
    {
        $head = new self($str);
        $this->__set_value(self::concat($head, $this, $this->_encoding)->_value);

        return $this;
    }

    /**
     * Конкатенация строк.
     *
     * @param str/string $head
     * @param str/string $tail
     * @param string $output_encoding
     * @return str
     */
    static function concat($head, $tail, $output_encoding = null)
    {
        $head = new self($head->_value, $output_encoding);
        $tail = new self($tail->_value, $output_encoding);

        $new_value = $head->_value . $tail->_value;

        return new self($new_value, $output_encoding);
    }

    /**
     * Возвращает фрагмент строки
     *
     * @param int $start
     * @param int $length
     * @return str
     */
    function substr($start, $length = null)
    {
        $start = $this->normalizeIndex($start);
        if (! $this->isExistsIndex($start)) {
            throw new ArgException();
        }
        if (! is_null($length)) {
            $length = $this->normalizeInt($length);
        } else {
            $length = $this->_length - $start;
        }
        return new self(mb_substr($this->_value, $start, $length, $this->_encoding), $this->_encoding);
    }

    /**
     * Преобразует в строчные.
     *
     * @return str
     */
    function toLowerCase()
    {
        $this->__set_value(mb_strtolower($this->_value, $this->_encoding));
        return $this;
    }

    /**
     * Преобразует в прописные.
     *
     * @return str
     */
    function toUpperCase()
    {
        $this->__set_value(mb_strtoupper($this->_value, $this->_encoding));
        return $this;
    }

    /**
     * Режет строку в массив по разделителю.
     *
     * @param string/str $delimiter Разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function split($delimiter, $limit = null)
    {
    	return $this->_eregSplit(false, $delimiter, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    function eregSplit($pattern, $limit = null)
    {
    	return $this->_eregSplit(true, $pattern, $limit);
    }

    /**
     * Режет строку в массив по regex-шаблону/разделителю.
     *
     * @access protected
     * @param bool $ereg Если true, то $delimiter воспринимается как regex-шаблон, иначе — как разделитель.
     * @param string/str $delimiter Шаблон/разделитель
     * @param int[optional] $limit Максимальное кол-во частей.
     * @return array of str
     */
    protected function _eregSplit($ereg, $delimiter, $limit = null)
    {
    	if (isset($limit) && (!is_int($limit) || $limit < 0)) {
    		throw new ArgException();
    	}
    	if ($limit === 0) {
    		return array();
    	}
    	$delimiter = new self($delimiter);
    	try {
    		$delimiter->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return array(clone $this);
    	}
    	if (! $ereg) {
	    	$delimiter = self::ereg_escape($delimiter);
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	if ($limit) {
    	   $tmp_result = mb_split($delimiter->_value, $this->_value, $limit);
    	} else {
    	   $tmp_result = mb_split($delimiter->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	$result = array();
    	foreach ($tmp_result as $str) {
    		$result[] = new self($str, $this->_encoding);
    	}

    	return $result;
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function ereg($pattern)
    {
    	return $this->_ereg(false, $pattern);
    }

    /**
     * Нечувствительный к регистру аналог ereg().
     *
     * @param string/str $pattern Шаблон
     * @return bool
     */
    function eregi($pattern)
    {
    	return $this->_ereg(true, $pattern);
    }

    /**
     * Проверяет, содержит ли строка подстроку $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function contains($substr)
    {
        return ($this->indexOf($substr) >= 0);
    }

    /**
     * Проверяет, начинается ли строка с подстроки $substr.
     *
     * @param string/str $substr Подстрока
     * @return bool
     */
    function startsWith($substr)
    {
        return ($this->indexOf($substr) == 0);
    }

    /**
     * Возвращает начальную позицию первого вложения подстроки $substr. Если вложения нет, — возвращает -1.
     *
     * @param string/str $substr
     * @param int[optional] $offset Смещение к концу относительно начала.
     * @return int
     */
    function indexOf($substr, $offset = null)
    {
    	$substr = new self($substr);
    	try {
            $substr->convertTo($this->_encoding);
    	} catch (Exception $e) {
    	    return -1;
    	}
    	if (isset($offset)) {
    	    if (!is_int($offset) || $offset < 0) {
        		throw new ArgException();
    	    }
        	if (($this->_length - $offset) < $substr->_length) {
        		return -1;
        	}
    	}
    	$result = mb_strpos($this->_value, $substr->_value, $offset, $this->_encoding);
    	if ($result === false) {
            $result = -1;
    	}
        return intval($result);
    }

    /**
     * Проверяет строку на соответствие regex-шаблону.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Шаблон
     * @return bool
     */
    protected function _ereg($case_insensetive, $pattern)
    {
    	$pattern = new self($pattern);
    	try {
    		$pattern->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return false;
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->_value;
    	    $subject = str($this)->convertTo("UTF-8")->_value;
    		$result = preg_match($ppp, $subject);
    	} else {
    		$result = mb_ereg($pattern->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	return (bool)$result;
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    function eregReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(false, $pattern, $replacement);
    }

    /**
     * Тоже что и eregReplace(), только нечувствительная к регистру.
     *
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    function eregiReplace($pattern, $replacement)
    {
    	return $this->_eregReplace(true, $pattern, $replacement);
    }

    /**
     * Заменяет все фрагменты строки, соответствующие regex-шаблону $pattern строкой $replacement.
     *
     * @access protected
     * @param bool $case_insensetive Если true, то нечувствительная к регистру.
     * @param string/str $pattern Regex-шаблон
     * @param string/str $replacement Подстановка
     * @return str
     */
    protected function _eregReplace($case_insensetive, $pattern, $replacement)
    {
    	$pattern = new self($pattern);
    	$replacement = new self($replacement);
    	try {
    		$pattern->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		return $this;
    	}
    	try {
    		$replacement->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		throw new Exception();
    	}

    	$old_encoding = mb_regex_encoding();
    	mb_regex_encoding($this->_encoding);
    	$tmp_result = "";
    	if ($case_insensetive) {
    	    $ppp = $pattern->replace("/", "\\/")->convertTo("UTF-8")->insertBefore("/")->append("/iu")->_value;
    	    $subject = str($this)->convertTo("UTF-8")->_value;
    		$tmp_result = preg_replace($ppp, $replacement->convertTo("UTF-8")->_value, $subject);
    		$tmp_result = str($tmp_result, "UTF-8")->convertTo($this->_encoding)->_value;
    	} else {
	    	$tmp_result = mb_ereg_replace($pattern->_value, $replacement->_value, $this->_value);
    	}
    	mb_regex_encoding($old_encoding);

    	$this->__set_value($tmp_result);

    	return $this;
    }

    /**
     * Удаление пробельных символов в начале строки.
     *
     * @return str
     */
    function lTrim()
    {
    	return $this->eregReplace("^\s+", "");
    }

    /**
     * Удаление пробельных символов в конце строки.
     *
     * @return str
     */
    function rTrim()
    {
    	return $this->eregReplace("\s+$", "");
    }

    /**
     * Удаление пробельных символов по краям строки.
     *
     * @return str
     */
    function trim()
    {
    	return $this->lTrim()->rTrim();
    }

    /**
     * Удаление пробельных символов в начале и в конце строки.
     * Так же слитие пробельных последовательностей внутри строки в один пробел.
     *
     * @return str
     */
    function normalizeSpace()
    {
    	return $this->trim()->eregReplace("\s+", " ");
    }

    /**
     * Экранирование последовательности символов для вставки в regex.
     *
     * @return str
     */
    static function ereg_escape($expr)
    {
		$expr = new self($expr);
    	$chars = array(
    		"\\", "+", "*", "?", "[", "^", "]", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":",
		);
		$result = new self("", $expr->_encoding);
        for ($i = 0; $i < $expr->_length; $i++) {
            if (in_array($expr->toChar($i), $chars, true)) {
                $result->append(new self("\\"));
                $result->append($expr->charAt($i));
            } else {
                $result->append($expr->charAt($i));
            }
        }

		return $result;
    }

    /**
     * Разбивает текст на массив строк по признаку конца строки.
     *
     * @param string/str $data
     * @param string/str $EOL Признак конца строки (можно только "\r\n", "\r" и "\n").
     * @return array of string
     */
    function splitText($EOL = null)
    {
        if (isset($EOL)) {
            $EOL = str($EOL)->toString();
            if (! in_array($EOL, array("\r\n", "\r", "\n"), true)) {
        		throw new ArgException();
            }
        }
    	$tmp_lines = $this->split(isset($EOL) ? $EOL : "\r\n");
    	if (isset($EOL)) {
    	    $lines = $tmp_lines;
    	} else {
    	    $lines = array();
    	    foreach ($tmp_lines as $tmp_line) {
    	        foreach ($tmp_line->split("\r") as $tl) {
                    $lines = ar::concat($lines, $tl->split("\n"));
    	        }
    	    }
    	}

        return $lines;
    }

    /**
     * Заменяет все подстроки $searche подстроками $replacement.
     *
     * @param string/str $searche
     * @param string/str $replacement
     * @return str
     */
    function replace($searche, $replacement)
    {
    	$searche = new self($searche);
    	$replacement = new self($replacement);
    	try {
            $searche->convertTo($this->_encoding);
    	} catch (Exception $e) {
    	    return $this;
    	}
    	try {
            $replacement->convertTo($this->_encoding);
    	} catch (Exception $e) {
    		throw new ArgException();
    	}
    	$searche = self::ereg_escape($searche);
        return $this->eregReplace($searche, $replacement);
    }

    /**
     * Преобразует символы в соответствующие XML-сущности.
     *
     * @param int $quote_style
     * ENT_COMPAT Преобразуются двойные кавычки, одиночные остаются без изменений.
     * ENT_QUOTES Преобразуются и двойные, и одиночные кавычки.
     * ENT_NOQUOTES И двойные, и одиночные кавычки остаются без изменений.
     *
     * @return str
     */
    function xmlEntities($quote_style = ENT_QUOTES)
    {
    	$this->replace("&", "&amp;")->replace("<", "&lt;")->replace(">", "&gt;");
    	if (($quote_style == ENT_QUOTES) || ($quote_style == ENT_COMPAT)) {
    	    $this->replace('"', "&quot;");
    	} elseif ($quote_style == ENT_QUOTES) {
    	    $this->replace("'", "&apos;");
    	}

    	return $this;
      }

    /**
     * Заменяет все управляющие символы (0—31) экранированными последовательностями \NN.
     *
     * @param string/str $addition_chars Строка с дополнительно экранируемыми символами (слеш будет экранирован всегда).
     * @return str
     */
    function addslashes($addition_chars = "")
    {
    	$addition_chars = new self($addition_chars);
    	$addition_chars_array = array();
        for ($i = 0; $i < $addition_chars->_length; $i++) {
            $addition_chars_array[] = $addition_chars->toChar($i);
        }
        $addition_chars_array[] = "\\";
    	$result = new self("");
        for ($i = 0; $i < $this->_length; $i++) {
            $char = $this->toChar($i);
            if (in_array($char, $addition_chars_array)) {
                $result->append(new self("\\"));
                $result->append($char);
            } elseif ((strlen($char) == 1) && (ord($char) < 32)) {
                $code = "";
                switch (ord($char)) {

                case 0x07:
                    $code = "a";
                    break;

                case 0x08:
                    $code = "b";
                    break;

                case 0x09:
                    $code = "t";
                    break;

                case 0x0A:
                    $code = "n";
                    break;

                case 0x0B:
                    $code = "v";
                    break;

                case 0x0C:
                    $code = "f";
                    break;

                case 0x0D:
                    $code = "r";
                    break;

                default:
                    $code = strval(intval(ord($char), 8));
                }
                $result->append(new self("\\"));
                $result->append($code);
            } else {
                $result->append($char);
            }
        }
        $this->__set_value($result->_value);

        return $this;
    }

    /**
     * Возвращает код символа в системе unicode по его номеру (с нуля).
     *
     * @param int $index
     * @return int
     */
    function charCodeAt($index)
    {
        $o = mb_convert_encoding($this->charAt($index), 'byte4be', $this->_encoding);
        return (ord($o[0]) << 24) + (ord($o[1]) << 16) + (ord($o[2]) << 8) + ord($o[3]);
    }

    /**
     * Возвращает код символа в системе Unicode.
     *
     * @param int $index
     * @return int
     */
    static function getCode($char)
    {
        return str($char)->charCodeAt(0);
    }

    /**
     * Возвращает символ по его коду Unicode.
     *
     * @param int $code
     * @return string
     */
    static function getChar($code)
    {
        if (! is_numeric($code)) {
            throw new ArgException();
        }
        $code = intval($code);
        if ($code < 0) {
            throw new ArgException();
        }
        $octets = chr(($code >> 24) % 256).chr(($code >> 16) % 256).chr(($code >> 8) % 256).chr($code % 256);
        return new self(mb_convert_encoding($octets, self::$innerEncoding, 'byte4be'), self::$innerEncoding);
    }

    /**
     * Создает строку из символов, заданных кодами Unicode.
     *
     * В JavaScrip’е этот метод принимает только двухбайтные коды
     * (если больше — берётся остаток от деления на 65536).
     * Так же как и эскейп-последовательности в строковом литерале \uHHHH.
     *
     * @param int $code1, $code2,… $codeN
     * @return string
     */
    static function fromCharCode()
    {
        $result = new self("", self::$innerEncoding);
        $args =& func_get_args();
        foreach ($args as $code) {
            $result->append(self::getChar($code));
        }
        return $result;
    }

    /**
     * Соединяет массив строк в одну с использованием разделителя.
     *
     * @param string $glue
     * @param array $pieces
     * @return int
     */
    static function implode($glue, $pieces)
    {
        if (! ar::isArray($pieces)) {
            throw new ArgException();
        }
        $glue = new self($glue);

        $result = new self("");

        $count = 0;
        foreach ($pieces as $v) {
            if ($count++) {
                $result->append($glue);
            }
            $result->append($v);
        }

        return $result;
    }

    /**
     * Возвращает строку из двоичных данных (октетов).
     *
     * @param string $stream
     * @return str
     */
    static function fromOctets($stream)
    {
        return new self($stream, 'ISO-8859-1');
    }

    /**
     * Нормализует целочисленное значение.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeInt($value)
    {
        if (! is_int($value)) {
            if (! is_numeric($value)) {
                throw new ArgException();
            } else {
                $value = intval($value);
            }
        }

        return $value;
    }

    /**
     * Нормализует индекс.
     *
     * @param mixed $index
     * @return int
     */
    protected function normalizeIndex($index)
    {
        return $this->normalizeInt($index);
    }

    /**
     * Проверяет существует ли указанный индекс.
     *
     * @param int $index
     * @return bool
     */
    protected function isExistsIndex($index)
    {
        return (($index < $this->_length) && ($index >= 0));
    }
}

/**
 * Создаёт/копирует объект str
 *
 * @param string/str $value
 * @param string[optional] $encoding
 * @return str
 */
function &str($value, $encoding = null)
{
    $args = func_get_args();
    return ____createO(__FUNCTION__, $args);
}

class StrEncodingUTF8 extends StrEncodingAbstract
{
    protected function validate()
    {
        /*
        UTF8-octets =   (UTF8-1 | UTF8-2 | UTF8-3 | UTF8-4)*
        UTF8-1      =   [#x00-7F]
        UTF8-2      =   [#xC2-DF] UTF8-tail  $sequenceVariant = 1

        UTF8-3      =   [#xE1-EC#xEE-EF] UTF8-tail UTF8-tail  $sequenceVariant = 1
                      | [#xE0]           [#xA0-BF] UTF8-tail  $sequenceVariant = 2
                      | [#xED]           [#x80-9F] UTF8-tail  $sequenceVariant = 3

        UTF8-4      =   [#xF1-F3]        UTF8-tail UTF8-tail UTF8-tail  $sequenceVariant = 1
                      | [#xF0]           [#x90-BF] UTF8-tail UTF8-tail  $sequenceVariant = 2
                        [#xF4]           [#x80-8F] UTF8-tail UTF8-tail  $sequenceVariant = 3

        UTF8-tail   =   [#x80-BF]
       */
        $octets_num = strlen($this->_value);

        $state = 0;
        $charsNum = 0;
        $octetsPerChar = 1; // Инициализация
        $sequenceVariant = 1; // Инициализация

        /////////////////
        $positionMax = 0xFFFFFFFF;
        $useIndex = ($octets_num - 1 <= $positionMax);
        if ($useIndex) {
            $indexInterval = $this->_indexOfOctetsInterval;
            $indexOfOctets =& $this->_indexOfOctets;
        }
        /////////////////
        for ($i = 0; $i < $octets_num; $i++) {
            $octet = ord($this->_value[$i]);

            if ($state == 0) {
                //echo sprintf("%08b", $octet), " ", sprintf("%02X", $octet), " ";
                $octetsPerChar = 1;
                $sequenceVariant = 1;
                if ($octet > 0x7F) {
                    /**
                     * Определяем нетерминал: UTF8-2, UTF8-3 или UTF8-4
                     * (Индекс нетерминала будет хранится в $octetsPerChar (1 — 4).)
                     *
                     * Нетерминалы UTF8-3 или UTF8-4 имеют три варианта продолжения.
                     * Вариант хранится в $sequenceVariant (1 — 3).
                     */
                    if ($octet <= 0xDF) {
                        if ($octet < 0xC2) {
                            return false;
                        }
                        $octetsPerChar = 2;
                    } elseif ($octet == 0xE0) {
                        $octetsPerChar = 3;
                        $sequenceVariant = 2;
                    } elseif ($octet >= 0xE1) {
                        if ($octet <= 0xEC) {
                            $octetsPerChar = 3;
                        } elseif ($octet == 0xED) {
                            $octetsPerChar = 3;
                            $sequenceVariant = 3;
                        } else {
                            if ($octet <= 0xEF) {
                                $octetsPerChar = 3;
                            } elseif ($octet == 0xF0) {
                                $octetsPerChar = 4;
                                $sequenceVariant = 2;
                            } elseif ($octet >= 0xF1) {
                                if ($octet <= 0xF3) {
                                    $octetsPerChar = 4;
                                } elseif ($octet == 0xF4) {
                                    $octetsPerChar = 4;
                                    $sequenceVariant = 3;
                                } else {
                                    return false;
                                }
                            }
                        }
                    } else {
                        return false;
                    }
                    $state = 1;
                }
                //echo "=> {$octetsPerChar}, {$sequenceVariant}\n";

                /////////////////
                if ($useIndex) {
                    if (($charsNum + $indexInterval) % $indexInterval == 0) {
                        $code = $i;
                        $indexOfOctets .= chr(($code >> 24) % 256).chr(($code >> 16) % 256).chr(($code >> 8) % 256).chr($code % 256);
                    }
                }
                /////////////////

                $charsNum++;
            } else {
                if (($state > 1) || ($sequenceVariant == 1)) {
                    if (($octet & 0x80) == 0x80)  { // 8-й бит = 1
                        if (($octet & 0x40) != 0x40) { // 7-й бит = 0
                            if ($state < $octetsPerChar - 1) {
                                $state++;
                            } else {
                                $state = 0;
                            }
                        } else {
                            return false;
                        }
                    } else {
                        return false;
                    }
                } elseif ($octetsPerChar == 3) {
                    if ($sequenceVariant == 2) {
                        if (($octet >= 0xA0) && ($octet <= 0xBF)) {
                            $state++;
                        } else {
                            return false;
                        }
                    } else { // $sequenceVariant == 3
                        if (($octet >= 0x80) && ($octet <= 0x9F)) {
                            $state++;
                        } else {
                            return false;
                        }
                    }
                } else { // $octetsPerChar == 4
                    if ($sequenceVariant == 2) {
                        if (($octet >= 0x90) && ($octet <= 0xBF)) {
                            $state++;
                        } else {
                            return false;
                        }
                    } else { // $sequenceVariant == 3
                        if (($octet >= 0x80) && ($octet <= 0x8F)) {
                            $state++;
                        } else {
                            return false;
                        }
                    }
                }
            }
        }

        if ($state != 0) {
            return false;
        }

        $this->_length = $charsNum;

        return true;
    }

    function __construct($stream)
    {
        parent::__construct($stream, 'UTF-8');

        if (! $this->validate()) {
            throw new Exception();
        }
    }

    /// Indexing /////
    /**
     * Обнулять при изменении строки!
     */
    private $_indexOfOctets = "";

    /**
     * Не изменяется во время жизни объекта!
     */
    private $_indexOfOctetsInterval = 16;
    /////////////////

    /**
     * Обнулять при изменении строки!
     */
    private $_octetIndex = 0;

    /**
     * Обнулять при изменении строки!
     */
    private $_charIndex = 0;

    function charAt2($index)
    {
        if (! is_int($index)) {
            if (! is_numeric($index)) {
                throw new ArgException();
            } else {
                $index = intval($index);
            }
        }

        if (! (($index < $this->_length) && ($index >= 0))) {
            throw new ArgException();
        }

        //////////////////////////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////
        //////////////////////////////////////////////////////////////////
        $octetsNum = strlen($this->_value);

        $octetIndex =& $this->_octetIndex;
        $charIndex =& $this->_charIndex;

        $prevCharIndex = 0;
        $nextCharIndex = $this->_length - 1;
        $prevOctetIndex = 0;
        $nextOctetIndex = $octetsNum - 1;

        $numOctetsPerChar = 1;

        /// Indexing /////
        if (strlen($this->_indexOfOctets) > 0) {
            $indexOfindex = intval(floor($index / $this->_indexOfOctetsInterval));
            if ($indexOfindex > 0) {
                $prevCharIndex = $indexOfindex * $this->_indexOfOctetsInterval;
                $o = substr($this->_indexOfOctets, $indexOfindex * 4, 4);
                $prevOctetIndex = (ord($o[0]) << 24) + (ord($o[1]) << 16) + (ord($o[2]) << 8) + ord($o[3]);
            } else {
                $prevCharIndex = 0;
                $prevOctetIndex = 0;
            }

            $indexOfindex = intval(ceil($index / $this->_indexOfOctetsInterval));
            if (($indexOfindex * $this->_indexOfOctetsInterval) < ($this->_length - 1)) {
                $nextCharIndex = $indexOfindex * $this->_indexOfOctetsInterval;
                $o = substr($this->_indexOfOctets, $indexOfindex * 4, 4);
                $nextOctetIndex = (ord($o[0]) << 24) + (ord($o[1]) << 16) + (ord($o[2]) << 8) + ord($o[3]);
            } else {
                $nextCharIndex = $this->_length - 1;
                $nextOctetIndex = strlen($this->_value) - 1;
            }
        }
        ////////////////////////////////////////

        if ($index >= $charIndex) {
            $prevCharIndex = $charIndex;
            $prevOctetIndex = $octetIndex;
        } elseif ($charIndex > 0) {
            $nextCharIndex = $charIndex;
            $nextOctetIndex = $octetIndex;
        }

        /**
         * Это эмпирическая величина.
         * Означает восколько раз поиск вперёд эффективнее поиска назад.
         */
        $forwardFora = 3.0;
        if (($index - $prevCharIndex) < (($nextCharIndex - $index) * $forwardFora)) {
            $charIndex = $prevCharIndex;
            $octetIndex = $prevOctetIndex;
        } else {
            $charIndex = $nextCharIndex;
            $octetIndex = $nextOctetIndex;
        }

        if ($charIndex < $index) {
            while (($octetIndex < $octetsNum - 1) && ($charIndex < $index)) {

                $octet = ord($this->_value[$octetIndex]);

                $result = 1;

                if (($octet & 0x80) == 0x80)  { // 8-й бит = 1
                    $result++;
                    if (($octet & 0x20) == 0x20) { // 6-й бит = 1
                        $result++;
                        if (($octet & 0x10) == 0x10) { // 5-й бит = 1
                            $result++;
                        }
                    }
                }

                $octet = ord($this->_value[$octetIndex += $result]);
                $charIndex++;
            }
        } elseif ($charIndex > $index) {
            while (($octetIndex > 0) && ($charIndex > $index)) {
                $octet = ord($this->_value[--$octetIndex]);
                $charIndex--;
                if (($octet & 0x80) == 0x80)  { // 8-й бит = 1
                    do {
                        $octet = ord($this->_value[--$octetIndex]);
                    } while (($octet & 0x40) != 0x40); // 7-й бит = 0
                }
            }
        }

        $octet = ord($this->_value[$octetIndex]);

        $result = 1;

        if (($octet & 0x80) == 0x80)  { // 8-й бит = 1
            $result++;
            if (($octet & 0x20) == 0x20) { // 6-й бит = 1
                $result++;
                if (($octet & 0x10) == 0x10) { // 5-й бит = 1
                    $result++;
                }
            }
        }

        return substr($this->_value, $octetIndex, $result);
    }

    function charAt3($index)
    {
        return $this->charAt2($index);
    }

    function charCodeAt2($index)
    {
        $o = $this->charAt2($index);

        switch (strlen($o)) {

        case 1:
            return ord($o);

        case 2:
            return ((ord($o[0]) << 26) >> 20) + ((ord($o[1]) << 25) >> 25);

        case 3:
            return ((ord($o[0]) << 27) >> 15) + ((ord($o[1]) << 25) >> 19) + ((ord($o[2]) << 25) >> 25);

        case 4:
            return ((ord($o[0]) << 28) >> 10) + ((ord($o[1]) << 25) >> 13) + ((ord($o[2]) << 25) >> 19) + ((ord($o[3]) << 25) >> 25);
        }
    }
}

?>